﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
using DotNetX;
using RPGX.Graphics;
using Microsoft.Xna.Framework.Input;
using RPGXEngine;

namespace RPGX.Graphics
{
    public class HUD
    {
        #region Properties

        public bool IsLoaded
        {
            get { return _isLoaded; }
            private set { _isLoaded = value; }
        }
        private bool _isLoaded = false;

        public List<HUDObject> ChildObjects
        {
            get { return this._childObjects; }
            private set { this._childObjects = value; }
        }
        private List<HUDObject> _childObjects = new List<HUDObject>();

        public Dictionary<string, HUDObject> ObjectsById
        {
            get { return _objectsById; }
        }
        private Dictionary<string, HUDObject> _objectsById = new Dictionary<string, HUDObject>();

        public GraphicsSystem Graphics
        {
            get { return _graphics; }
            set { _graphics = value; }
        }
        private GraphicsSystem _graphics = null;

        public GameText Text
        {
            get { return _gameText; }
            set { _gameText = value; }
        }
        private GameText _gameText = null;

        public ScriptManager Script
        {
            get { return _script; }
            set { _script = value; }
        }
        private ScriptManager _script = null;

        public int Width
        {
            get
            {
                return this.Graphics.GDM.GraphicsDevice.Viewport.TitleSafeArea.Width;
            }
        }

        public int Height
        {
            get
            {
                return this.Graphics.GDM.GraphicsDevice.Viewport.TitleSafeArea.Height;
            }
        }

        public InputMouse Mouse
        {
            get { return _mouse; }
            set { _mouse = value; }
        }
        private InputMouse _mouse = null;

        public Dictionary<string, MouseType> MouseTypes
        {
            get { return _mouseTypes; }
            set { _mouseTypes = value; }
        }
        private Dictionary<string, MouseType> _mouseTypes = new Dictionary<string, MouseType>();

        public string DefaultMouseType
        {
            get
            {
                if(String.IsNullOrEmpty(this._defaultMouseType))
                {
                    if (this.MouseTypes.Keys.Count == 0)
                        return String.Empty;
                    else
                        return this.MouseTypes.Keys.ToList()[0];
                }
                else
                {
                    return _defaultMouseType;
                }
            }
            set { _defaultMouseType = value; }
        }
        private string _defaultMouseType = String.Empty;

        public HUDObject CurrentMouseOverObject
        {
            get { return _currentMouseOverObject; }
            set { _currentMouseOverObject = value; }
        }
        private HUDObject _currentMouseOverObject = null;

        public HUDObject CurrentMouseClickObject
        {
            get { return _currentMouseClickObject; }
            set { _currentMouseClickObject = value; }
        }
        private HUDObject _currentMouseClickObject = null;

        public HUDObject CurrentFocusObject
        {
            get { return _currentFocusObject; }
            set { _currentFocusObject = value; }
        }
        private HUDObject _currentFocusObject = null;

        public Dictionary<Keys, KeyState> CurrentKeyState
        {
            get { return _currentKeyState; }
            set { _currentKeyState = value; }
        }
        private Dictionary<Keys, KeyState> _currentKeyState = new Dictionary<Keys, KeyState>();

        public List<string> Announcements
        {
            get { return _announcements; }
            set { _announcements = value; }
        }
        private List<string> _announcements = new List<string>();

        #endregion

        #region Public Methods

        public bool Load(string settingsFileName, string fileName, GraphicsSystem graphics, GameText gameText, ScriptManager script)
        {
            if (graphics == null)
                throw new ArgumentNullException("'graphics' is null.");

            if (gameText == null)
                throw new ArgumentNullException("'gameText' is null.");

            if (script == null)
                throw new ArgumentNullException("'script' is null.");

            this.UnLoad();

            this.Graphics = graphics;
            this.Text = gameText;
            this.Script = script;

            this.LoadSettings(settingsFileName);

            XDocument doc = XDocument.Load(fileName);

            XElement elementHUD = doc.Element("hud");

            this.ChildObjects = this.LoadChildElements(elementHUD, null);

            if (this.MouseTypes.Count > 0)
            {
                this.Mouse = new InputMouse();
                this.Mouse.Load(this.MouseTypes[this.DefaultMouseType]);
            }

            this.SetupKeyboard();

            this.IsLoaded = true;

            return true;
        }

        public List<HUDObject> LoadChildElements(XElement element, HUDObject parent)
        {
            List<HUDObject> es = new List<HUDObject>();

            foreach (var e in element.Elements())
            {
                HUDObject o = HUDControlFactory.Create(e.Name.LocalName);
                this.LoadStandardHUDObjectProperties(e, o);

                o.HUD = this;
                o.Parent = parent;

                o.FromXML(e, this);

                es.Add(o);
                this.ObjectsById.Add(o.Id, o);
            }

            return es;
        }

        public bool UnLoad()
        {
            foreach (var o in this.ChildObjects)
            {
                o.UnLoad();
            }

            this.ChildObjects.Clear();
            this.ObjectsById.Clear();

            if (this.Mouse != null)
            {
                this.Mouse.UnLoad();
                this.Mouse = null;
            }

            this.IsLoaded = false;

            return true;
        }

        public void Update(GameTime time)
        {
            if (this.Mouse != null)
            {
                this.Mouse.Update(time);

                this.CheckForMouseEvents(time);
            }

            this.CheckForKeyboardEvents(time);

            foreach (var o in this.ChildObjects)
            {
                o.Update(time);
            }
        }

        public void SetMouseType(string type)
        {
            if (!this.MouseTypes.Keys.ToList().Contains(type))
                return;

            this.Mouse.SetType(this.MouseTypes[type]);
        }

        #endregion

        #region Internal Methods

        internal bool Render(GameTime time, SpriteBatch spriteBatch)
        {
            if (!this.IsLoaded)
                return false;

            int height = (int)this.Text.CurrentFont.MeasureString(" ").Y;
            int y = 0;
            for (int i = this.Announcements.Count - 1; i >= 0; i--)
            {
                spriteBatch.DrawString(this.Text.CurrentFont, this.Announcements[i], new Vector2(10, y), Color.Red);
                y += height;
            }

            foreach(var c in this.ChildObjects)
            {
                Rectangle bounds = new Rectangle();
                bounds.X = 0;
                bounds.Y = 0;
                bounds.Width = this.Graphics.Game.GraphicsDevice.Viewport.TitleSafeArea.Width;
                bounds.Height = this.Graphics.Game.GraphicsDevice.Viewport.TitleSafeArea.Height;

                c.Render(time, spriteBatch, bounds);
            }

            if (this.Mouse != null)
            {
                this.Mouse.Render(spriteBatch);
            }

            return true;
        }

        internal void LoadStandardHUDObjectProperties(XElement e, HUDObject o)
        {
            o.Id = e.Attribute("id").Value;

            float x = e.Attribute("x").Value.ToFloatOrZero();
            float y = e.Attribute("y").Value.ToFloatOrZero();

            o.PositionRelativeToParent = new Vector2(x, y);

            string width = e.Attribute("width").Value.Trim();
            string height = e.Attribute("height").Value.Trim();

            if (width.EndsWith("%"))
            {
                width = width.Replace("%", String.Empty);
                o.WidthAsPercentageOfParent = width.ToInt32OrZero();
            }
            else
            {
                o.Width = width.ToInt32OrZero();
            }

            if (height.EndsWith("%"))
            {
                height = height.Replace("%", String.Empty);
                o.HeightAsPercentageOfParent = height.ToInt32OrZero();
            }
            else
            {
                o.Height = height.ToInt32OrZero();
            }

            var mouseType = e.Attribute("mousetype");
            if (mouseType != null && !String.IsNullOrEmpty(mouseType.Value))
            {
                o.MouseType = mouseType.Value;
            }

            var onMouseOverScriptAction = e.Attribute("onmouseover");
            if (onMouseOverScriptAction != null && !String.IsNullOrEmpty(onMouseOverScriptAction.Value))
            {
                o.OnMouseOverScriptAction = onMouseOverScriptAction.Value;
            }

            var onMouseEnterScriptAction = e.Attribute("onmouseenter");
            if (onMouseEnterScriptAction != null && !String.IsNullOrEmpty(onMouseEnterScriptAction.Value))
            {
                o.OnMouseEnterScriptAction = onMouseEnterScriptAction.Value;
            }

            var onMouseLeaveScriptAction = e.Attribute("onmouseleave");
            if (onMouseLeaveScriptAction != null && !String.IsNullOrEmpty(onMouseLeaveScriptAction.Value))
            {
                o.OnMouseLeaveScriptAction = onMouseLeaveScriptAction.Value;
            }

            var onMouseClickScriptAction = e.Attribute("onmouseclick");
            if (onMouseClickScriptAction != null && !String.IsNullOrEmpty(onMouseClickScriptAction.Value))
            {
                o.OnMouseClickScriptAction = onMouseClickScriptAction.Value;
            }

            var onMouseDownScriptAction = e.Attribute("onmousedown");
            if (onMouseDownScriptAction != null && !String.IsNullOrEmpty(onMouseDownScriptAction.Value))
            {
                o.OnMouseDownScriptAction = onMouseDownScriptAction.Value;
            }

            var onMouseUpScriptAction = e.Attribute("onmouseup");
            if (onMouseUpScriptAction != null && !String.IsNullOrEmpty(onMouseUpScriptAction.Value))
            {
                o.OnMouseUpScriptAction = onMouseUpScriptAction.Value;
            }

            var onFocusInScriptAction = e.Attribute("onfocusin");
            if (onFocusInScriptAction != null && !String.IsNullOrEmpty(onFocusInScriptAction.Value))
            {
                o.OnFocusInScriptAction = onFocusInScriptAction.Value;
            }

            var onFocusOutScriptAction = e.Attribute("onfocusout");
            if (onFocusOutScriptAction != null && !String.IsNullOrEmpty(onFocusOutScriptAction.Value))
            {
                o.OnFocusOutScriptAction = onFocusOutScriptAction.Value;
            }
        }

        #endregion

        #region Private Methods

        private void LoadSettings(string settingsFileName)
        {
            XDocument doc = XDocument.Load(settingsFileName);

            foreach (var mouseSetting in doc.Element("settings").Element("hud").Element("mousetypes").Elements("mousetype"))
            {
                MouseType mt = new MouseType();
                mt.Name = mouseSetting.Element("name").Value;
                mt.Texture = this.Graphics.Game.Content.Load<Texture2D>(mouseSetting.Element("texture").Value);
                mt.Width = mouseSetting.Element("width").Value.ToInt32OrZero();
                mt.Height = mouseSetting.Element("height").Value.ToInt32OrZero();

                if(!this.MouseTypes.ContainsKey(mt.Name))
                    this.MouseTypes.Add(mt.Name, mt);
            }

            var defaultMouseType = doc.Element("settings").Element("hud").Attribute("mousetype");
            if (defaultMouseType != null && !String.IsNullOrEmpty(defaultMouseType.Value) && this.MouseTypes.Keys.Contains(defaultMouseType.Value))
            {
                this.DefaultMouseType = defaultMouseType.Value;
            }
        }

        private void CheckForKeyboardEvents(GameTime time)
        {
            var keyboardState = Keyboard.GetState();

            var pressedKeys = keyboardState.GetPressedKeys().ToList();

            this.KeyboardKeyDown(pressedKeys);

            foreach (var k in pressedKeys)
            {
                this.CurrentKeyState[k] = KeyState.Down;
            }

            var keysUp = (from k in this.CurrentKeyState
                          where k.Value == KeyState.Down &&
                                !pressedKeys.Contains(k.Key)
                          select k.Key).ToList();

            this.KeyboardKeyUp(keysUp);
            this.KeyboardKeyPressed(keysUp); // these are seperate because another key 'press' could be when a key is down for 1 seconds, but that is a TBI

            foreach (var k in keysUp)
            {
                this.CurrentKeyState[k] = KeyState.Up;
            }
        }

        private void CheckForMouseEvents(GameTime time)
        {
            var mouseState = Microsoft.Xna.Framework.Input.Mouse.GetState();

            this.CheckForMouseOver(time, mouseState);
            this.CheckForMouseEnterAndLeave(time, mouseState);

            this.CheckForMouseClickAndUpAndFocus(time, mouseState);
            this.CheckForMouseDown(time, mouseState);
        }

        private void CheckForMouseDown(GameTime time, MouseState mouseState)
        {
            if (mouseState.LeftButton == ButtonState.Released)
                return;

            Point localMousePosition = new Point();

            HUDObject o = this.GetObjectAtPosition(new Vector2(mouseState.X, mouseState.Y), new Vector2(0, 0), null, ref localMousePosition);
            if (o == null)
                return;

            HUDMouseDownEventArgs e = new HUDMouseDownEventArgs();
            e.MousePosition = new Point(mouseState.X, mouseState.Y);
            e.LocalMousePosition = localMousePosition;

            o.OnMouseDown(e);
        }

        private void CheckForMouseClickAndUpAndFocus(GameTime time, MouseState mouseState)
        {
            Point localMousePosition = new Point();

            HUDObject o = this.GetObjectAtPosition(new Vector2(mouseState.X, mouseState.Y), new Vector2(0, 0), null, ref localMousePosition);
            if (o == null)
            {
                return;
            }

            if (mouseState.LeftButton == ButtonState.Pressed)
            {
                if (this.CurrentMouseClickObject == null)
                {
                    this.CurrentMouseClickObject = o;
                }
            }
            else if(mouseState.LeftButton == ButtonState.Released)
            {
                // ensure that the user hasn't mouse downed on one object then moved the mouse to another then mouse upped
                if (this.CurrentMouseClickObject != null)
                {
                    if (this.CurrentMouseClickObject.Id == o.Id)
                    {
                        HUDMouseClickEventArgs e = new HUDMouseClickEventArgs();
                        e.MousePosition = new Point(mouseState.X, mouseState.Y);
                        e.LocalMousePosition = localMousePosition;

                        this.CurrentMouseClickObject.OnMouseClick(e);

                        if (this.CurrentFocusObject != null)
                        {
                            this.CurrentFocusObject.IsFocused = false;

                            HUDFocusOutEventArgs foe = new HUDFocusOutEventArgs();
                            foe.MousePosition = new Point(mouseState.X, mouseState.Y);
                            foe.LocalMousePosition = localMousePosition;

                            this.CurrentFocusObject.OnFocusOut(foe);
                        }

                        this.CurrentFocusObject = o;

                        HUDFocusInEventArgs fie = new HUDFocusInEventArgs();
                        fie.MousePosition = new Point(mouseState.X, mouseState.Y);
                        fie.LocalMousePosition = localMousePosition;

                        this.CurrentFocusObject.IsFocused = true;

                        this.CurrentFocusObject.OnFocusIn(fie);
                    }

                    HUDMouseUpEventArgs ue = new HUDMouseUpEventArgs();
                    ue.MousePosition = new Point(mouseState.X, mouseState.Y);
                    ue.LocalMousePosition = localMousePosition;

                    // mouse up happens at the hud object the mouse button is released, unlike mouse click
                    // which only happens on same object as the mouse was downed
                    o.OnMouseUp(ue);
                }

                this.CurrentMouseClickObject = null;
            }
        }

        private void CheckForMouseEnterAndLeave(GameTime time, MouseState mouseState)
        {
            Point localMousePosition = new Point();

            HUDObject o = this.GetObjectAtPosition(new Vector2(mouseState.X, mouseState.Y), new Vector2(0, 0), null, ref localMousePosition);
            if (o == null)
            {
                if (this.CurrentMouseOverObject != null)
                {
                    HUDMouseLeaveEventArgs e = new HUDMouseLeaveEventArgs();
                    e.MousePosition = new Point(mouseState.X, mouseState.Y);
                    e.LocalMousePosition = localMousePosition;

                    this.CurrentMouseOverObject.OnMouseLeave(e);

                    this.CurrentMouseOverObject = null;
                }
            }
            else
            {
                if (this.CurrentMouseOverObject == null)
                {
                    this.CurrentMouseOverObject = o;

                    HUDMouseEnterEventArgs e = new HUDMouseEnterEventArgs();
                    e.MousePosition = new Point(mouseState.X, mouseState.Y);
                    e.LocalMousePosition = localMousePosition;

                    this.CurrentMouseOverObject.OnMouseEnter(e);
                }
                else
                {
                    if (this.CurrentMouseOverObject.Id != o.Id)
                    {
                        HUDMouseLeaveEventArgs le = new HUDMouseLeaveEventArgs();
                        le.MousePosition = new Point(mouseState.X, mouseState.Y);
                        le.LocalMousePosition = localMousePosition;

                        this.CurrentMouseOverObject.OnMouseLeave(le);

                        this.CurrentMouseOverObject = o;

                        HUDMouseEnterEventArgs e = new HUDMouseEnterEventArgs();
                        e.MousePosition = new Point(mouseState.X, mouseState.Y);
                        e.LocalMousePosition = localMousePosition;

                        this.CurrentMouseOverObject.OnMouseEnter(e);
                    }
                }
            }
        }

        private void CheckForMouseOver(GameTime time, MouseState mouseState)
        {
            Point localMousePosition = new Point();

            HUDObject o = this.GetObjectAtPosition(new Vector2(mouseState.X, mouseState.Y), new Vector2(0, 0), null, ref localMousePosition);
            if (o == null)
            {
                this.SetMouseType(this.DefaultMouseType);
                return;
            }

            string mouseType = this.DefaultMouseType;

            if (!String.IsNullOrEmpty(o.MouseType))
            {
                mouseType = o.MouseType;
            }

            this.SetMouseType(mouseType);

            HUDMouseOverEventArgs e = new HUDMouseOverEventArgs();
            e.MousePosition = new Point(mouseState.X, mouseState.Y);
            e.LocalMousePosition = localMousePosition;

            o.OnMouseOver(e);
        }

        private HUDObject GetObjectAtPosition(Vector2 pos, Vector2 parentLocation, HUDObject parent, ref Point localMousePosition)
        {
            var childObjects = parent == null ? this.ChildObjects : parent.ChildObjects;

            foreach (var o in childObjects)
            {
                Vector2 loc = new Vector2(o.PositionRelativeToParent.X + parentLocation.X,
                                          o.PositionRelativeToParent.Y + parentLocation.Y);

                Rectangle bounds = new Rectangle((int)loc.X, (int)loc.Y, o.Width, o.Height);

                if (bounds.Contains((int)pos.X, (int)pos.Y))
                {
                    localMousePosition = new Point((int)(pos.X - loc.X), (int)(pos.Y - loc.Y));

                    var co = this.GetObjectAtPosition(pos, loc, o, ref localMousePosition);
                    if (co == null)
                        return o;
                    else
                        return co;
                }
            }

            return null;
        }

        private void SetupKeyboard()
        {
            foreach(Keys k in Enum.GetValues(typeof(Keys)))
            {
                if(!this.CurrentKeyState.ContainsKey(k))
                {
                    this.CurrentKeyState.Add(k, KeyState.Up);
                }
            }
        }

        private void KeyboardKeyDown(List<Keys> keys)
        {
            if (this.CurrentFocusObject == null)
                return;

            if (keys == null || keys.Count <= 0)
                return;

            HUDKeyDownEventArgs e = new HUDKeyDownEventArgs();
            e.Keys = keys;
            this.CurrentFocusObject.OnKeyDown(e);
        }

        private void KeyboardKeyUp(List<Keys> keys)
        {
            if (this.CurrentFocusObject == null)
                return;

            if (keys == null || keys.Count <= 0)
                return;

            HUDKeyUpEventArgs e = new HUDKeyUpEventArgs();
            e.Keys = keys;
            this.CurrentFocusObject.OnKeyUp(e);
        }

        private void KeyboardKeyPressed(List<Keys> keys)
        {
            if (this.CurrentFocusObject == null)
                return;

            if (keys == null || keys.Count <= 0)
                return;

            HUDKeyPressedEventArgs e = new HUDKeyPressedEventArgs();
            e.Keys = keys;
            this.CurrentFocusObject.OnKeyPressed(e);
        }

        #endregion
    }
}
